package jvs.peer.bt;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.Properties;

import jvs.peer.io.DataPacketService;
import jvs.peer.io.Message;
import jvs.peer.io.PooledInputStream;
import jvs.peer.io.PooledOutputStream;
import jvs.peer.util.Log;

import org.apache.commons.pool.KeyedPoolableObjectFactory;
import org.apache.commons.pool.impl.GenericKeyedObjectPool;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;

/**
 * @author qiangli
 * 
 */
public class Bluetooth extends DataPacketService {
	private static final String TAG = "Bluetooth";
	
	private static final int BUF_SIZE = 8192;

	class PooledSocket {

		private BluetoothSocket socket = null;
		private PooledInputStream inputStream = null;
		private PooledOutputStream outputStream = null;
		private boolean connected = false;
		
		private Thread receiver;

		public PooledSocket() {
			this.socket = null;
			this.inputStream = null;
			this.outputStream = null;
			this.connected = false;
		}

		private void startReceiver(final PooledInputStream in) {
			if (receiver != null) {
				receiver.interrupt();
				receiver = null;
			}
			receiver = new Thread() {
				public void run() {
					try {
						receive(in);
					} catch (Exception e) {
						e.printStackTrace();

						in.destroy();
					}
				}
			};
			receiver.setDaemon(true);
			receiver.start();
		}

		public void init(BluetoothSocket socket) throws IOException {
			if (this.socket != null) {
				destroy();
			}

			this.socket = socket;
			this.connected = true;

			//
			final PooledInputStream in = this.getInputStream();
			startReceiver(in);
			final OutputStream out = this.getOutputStream();
			out.flush();
		}

		public PooledInputStream getInputStream() throws IOException {
			if (inputStream == null) {
				try {
					inputStream = new PooledInputStream(socket.getInputStream());
				} catch (Exception e) {
					destroy();
				}
			}
			return inputStream;
		}

		public PooledOutputStream getOutputStream() throws IOException {
			if (outputStream == null) {
				try {
					outputStream = new PooledOutputStream(
							socket.getOutputStream());
				} catch (Exception e) {
					destroy();
				}
			}
			return outputStream;
		}

		public BluetoothDevice getRemoteDevice() {
			return socket.getRemoteDevice();
		}

		public void destroy() {
			connected = false;

			close(inputStream);
			close(outputStream);
			close(socket);

			if (receiver != null) {
				receiver.interrupt();
				receiver = null;
			}

			this.socket = null;
			this.inputStream = null;
			this.outputStream = null;
		}

		private void close(Closeable s) {
			if (s == null) {
				return;
			}
			try {
				s.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		public void flush() throws IOException {
			if (outputStream != null) {
				outputStream.flush();
			}
		}

	}

	private static int counter = 0;

	private class SocketPoolableObjectFactory implements
			KeyedPoolableObjectFactory {

		public Object makeObject(Object obj) throws Exception {
			final String key = obj.toString();
			if (Log.DEBUG) {
				Log.d(TAG, "####### [" + counter++ + "] " + key);
			}

			return new PooledSocket();
		}

		public void destroyObject(Object key, Object obj) throws Exception {
			if (Log.DEBUG) {
				Log.d(TAG, "destroyObject [" + counter + "] " + key);
			}
			PooledSocket so = (PooledSocket) obj;
			try {
				so.destroy();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		public boolean validateObject(Object key, Object obj) {
			if (Log.DEBUG) {
				Log.d(TAG, "validateObject [" + counter + "] " + key);
			}
			return true;
		}

		public void activateObject(Object key, Object obj) throws Exception {
			if (Log.DEBUG) {
				Log.d(TAG, "activateObject [" + counter + "] " + key);
			}
			PooledSocket so = (PooledSocket) obj;

			if (so.connected) {
				so.flush();
				return;
			}

			//
			if (adapter.isDiscovering()) {
				adapter.cancelDiscovery();
			}

			BluetoothSocket s = createSocket((String) key);
			s.connect();

			so.init(s);

			return;
		}

		public void passivateObject(Object key, Object obj) throws Exception {
			if (Log.DEBUG) {
				Log.d(TAG, "passivateObject [" + counter + "] " + key);
			}
			PooledSocket so = (PooledSocket) obj;
			so.flush();
		}

	}

	private SocketPoolableObjectFactory factory = new SocketPoolableObjectFactory();
	private GenericKeyedObjectPool pool = new GenericKeyedObjectPool(factory, 1);

	private final String local;
	private final boolean colonAddress;
	private final BluetoothAdapter adapter;

	public Bluetooth() throws Exception {
		adapter = BluetoothAdapter.getDefaultAdapter();

		local = adapter.getAddress();
		colonAddress = local.indexOf(":") > 0;
	}

	private static String convertAddress(String address, boolean colonAddress) {
		// Android wants colon, Bluecove accepts both
		if (address.indexOf(":") > 0 && colonAddress) {
			return address;
		}
		if (address.indexOf(":") < 0 && colonAddress == false) {
			return address;
		}
		// convert
		final int len = address.length();
		StringBuilder sb = new StringBuilder();
		if (colonAddress) {
			for (int i = 0; i < len; i += 2) {
				sb.append(address.charAt(i));
				sb.append(address.charAt(i + 1));
				if (i + 2 < len) {
					sb.append(":");
				}
			}
		} else {
			for (int i = 0; i < len; i++) {
				char ci = address.charAt(i);
				if (ci == ':') {
					continue;
				}
				sb.append(ci);
			}
		}
		return sb.toString();
	}

	@Override
	public void init(Properties props) throws Exception {
		super.init(props);

		if (Log.DEBUG) {
			Log.d(TAG, "init " + props);
		}
	}

	@Override
	public void startup() throws Exception {
		if (Log.DEBUG) {
			Log.d(TAG, "startup... " + this);
		}
		
		//
		pool.clear();

		//
		startupListener();
		//

		if (Log.DEBUG) {
			Log.d(TAG, "start completed " + this);
		}
	}

	@Override
	public void shutdown() throws Exception {
		if (Log.DEBUG) {
			Log.d(TAG, "shutdown... " + this);
		}
		
		//
		shutdownListener();
		//
		pool.clear();

		if (Log.DEBUG) {
			Log.d(TAG, "shutdown completed " + this);
		}
	}

	@Override
	public Message receive() throws IOException {
		Message msg = super.receive();

		if (Log.DEBUG) {
			Log.d(TAG, this + " revd<<< " + msg);
		}

		return msg;
	}

	@Override
	protected void onMessage(Message msg) throws InterruptedException {
		//tsIn = System.currentTimeMillis();
		
		super.onMessage(msg);
	}

	@Override
	public void send(Message msg) throws Exception {
		msg.getHeader().setFrom(local);

		final String peer = msg.getHeader().getPeer();
		final String key = convertAddress(peer, colonAddress);

		if (Log.DEBUG) {
			Log.d(TAG, this + " send peer: " + peer + " key: " + key);
		}

		PooledSocket sc = null;
		PooledOutputStream out = null;
		try {
			sc = (PooledSocket) pool.borrowObject(key);
			
			out = sc.getOutputStream();

			if (Log.DEBUG) {
				Log.d(TAG, this + " sending>>> " + msg);
			}

			send(msg, out);

			out.flush();
			//
			if (Log.DEBUG) {
				Log.d(TAG, this + " sent>>> ");
			}

		} catch (Exception e) {
			if (sc != null) {
				pool.invalidateObject(key, sc);
			}
			
			pool.clear(key);

			if (out != null) {
				out.destroy();
			}
			throw e;
		} finally {
			if (sc != null) {
				pool.returnObject(key, sc);
			}
		}
	}

	protected void receive(BluetoothSocket s) throws Exception {
		final String peer = s.getRemoteDevice().getAddress();
		final String key = convertAddress(peer, colonAddress);

		if (Log.DEBUG) {
			Log.d(TAG, "receive peer: " + peer + " key: " + key);
		}

		PooledSocket sc = null;
		try {
			sc = (PooledSocket) pool.borrowObject(key);
			
			sc.init(s);
			
		} catch (Exception e) {
			if (sc != null) {
				pool.invalidateObject(key, sc);
			}

			pool.clear(key);

			throw e;
		} finally {
			if (sc != null) {
				pool.returnObject(key, sc);
			}
		}
	}

	@Override
	public String getJid() {
		return local;
	}

	public BluetoothDevice getDevice(String address) throws Exception {
		return adapter.getRemoteDevice(address);
	}
	
	private ListenerThread listener;

	private class ListenerThread extends Thread {

		private boolean running = false;
		
		private BluetoothServerSocket conn = null;
		
		@Override
		public void interrupt() {
			running  = false;
			close(conn);
			
			super.interrupt();
		}

		public void run() {
			if (Log.DEBUG) {
				Log.d(TAG,
						this
								+ " ************************* listenUsingRfcommWithServiceRecord...");
			}
			
			running = true;
		
			if (Log.DEBUG) {
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ start");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ start");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ start");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ start");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ start");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ start");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ start");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ start");
			}
			

			try {
				conn = createServerSocket();	
			} catch (Exception e) {
				if (Log.DEBUG) {
					e.printStackTrace();
				}

				close(conn);
				conn = null;

				sleep();
				
				return;
			}

			while (running) {
				try {
					if (Log.DEBUG) {
						Log.d(TAG, this + " ********************* accepting connection..." + conn);
					}

					if (Log.DEBUG) {
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepting");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepting");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepting");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepting");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepting");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepting");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepting");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepting");
					}
					
					BluetoothSocket s = conn.accept();

					if (Log.DEBUG) {
						Log.d(TAG, this + " ************************* accepted: " + s);
					}


					if (Log.DEBUG) {
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepted");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepted");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepted");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepted");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepted");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepted");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepted");
						Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ accepted");
					}
					
					handle(s);

					if (Log.DEBUG) {
						Log.d(TAG, this + " *** handled: " + s);
					}
				} catch (Exception e) {
					e.printStackTrace();

					break;
				}
			}

			close(conn);
			
			running = false;
			
			if (Log.DEBUG) {
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ stop");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ stop");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ stop");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ stop");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ stop");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ stop");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ stop");
				Log.d(TAG, this + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ stop");
			}
		}

		private void sleep() {
			try {
				if (Log.DEBUG) {
					Log.d(TAG, "waiting... ");
				}
				
				Thread.sleep(60000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		private void handle(BluetoothSocket s) {
			try {
				receive(s);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	private void startupListener() {
		if (listener != null) {
			try {
				shutdown();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		//
		listener = new ListenerThread();
		listener.setDaemon(true);
		listener.start();
		
		setDiscoverableTimeout(120 * 1000);
	}

	private void shutdownListener() {
		try {
			listener.interrupt();
			listener = null;
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void close(Closeable c) {
		if (Log.DEBUG) {
			Log.d(TAG, this + " ******** closing " + c);
		}
		if (c == null) {
			return;
		}
		try {
			c.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private BluetoothServerSocket createServerSocket() throws Exception {
		//try use insecure
		BluetoothServerSocket s = null;
		boolean secure = true;
		Method m = null;
		try {
			m = BluetoothAdapter.class.getMethod("listenUsingInsecureRfcommWithServiceRecord", String.class, java.util.UUID.class);
			secure = false;
		} catch (Exception e) {
			// under API level 10
			//e.printStackTrace();
			if (Log.DEBUG) {
				Log.d(TAG, "createServerSocket: " + e );
			}
		}
		final String name = BluetoothConstants.HOTWEB_NAME;
		final java.util.UUID uuid = BluetoothConstants.HOTWEB_UUID;
		
		if (Log.DEBUG) {
			Log.d(TAG, "################################# secure: " + secure);
		}
		
		if (secure) {
			s = adapter.listenUsingRfcommWithServiceRecord(name, uuid);
		} else {
			s = (BluetoothServerSocket) m.invoke(adapter, name, uuid);
		}
		
		return s;
	}
	
	private BluetoothSocket createSocket(String address) throws Exception {
		//try create insecure
		final BluetoothDevice device = getDevice(address);

		if (Log.DEBUG) {
			Log.d(TAG, "createSocket device: " + device);
		}
		
		final java.util.UUID uuid = BluetoothConstants.HOTWEB_UUID;
		BluetoothSocket s = null;

		boolean secure = true;
		Method m = null;
		try {
			m = BluetoothDevice.class.getMethod("createInsecureRfcommSocketToServiceRecord", java.util.UUID.class);
			secure = false;
		} catch (Exception e) {
			//under API level 10
			if (Log.DEBUG) {
				Log.d(TAG, "createSocket: " + e );
			}
		}
		
		if (Log.DEBUG) {
			Log.d(TAG, "################################# secure: " + secure);
		}
		
		if (secure) {
			s = device.createRfcommSocketToServiceRecord(uuid);
		} else {
			s = (BluetoothSocket) m.invoke(device, uuid);
		}

		if (Log.DEBUG) {
			Log.d(TAG, "createSocket socket: " + s);
		}
		
		return s;
	}
	
	private void setDiscoverableTimeout(int timeout) {
		try {

			Method getter = BluetoothAdapter.class.getMethod("getDiscoverableTimeout");
			
			if (Log.DEBUG) {
				Log.d(TAG, "@@@@@@@@@@@@@@@@@@@@@@@ " + getter.invoke(adapter));
			}
			
			Method m = BluetoothAdapter.class.getMethod("setDiscoverableTimeout", int.class);
			m.invoke(adapter, timeout);
			
			
			if (Log.DEBUG) {
				Log.d(TAG, "@@@@@@@@@@@@@@@@@@@@@@@ " + getter.invoke(adapter));
			}
			
		} catch (Throwable e) {
			e.printStackTrace();
		}
	}

	public int getBufferSize() {
		return BUF_SIZE;
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
	}

}
